package com.ian.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.*;
import org.springframework.security.oauth2.client.endpoint.*;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/**
 * @author Witt
 * @version 1.0.0
 * @date 2022/5/17
 */
@Configuration
public class OAuth2ClientConfig {

    /**
     * 权限授予类型：authorizationCode（带refreshToken）
     * @param clientRegistrationRepository
     * @param authorizedClientRepository
     * @return
     */
     @Bean
     public OAuth2AuthorizedClientManager authorizedClientManager3(
             ClientRegistrationRepository clientRegistrationRepository,
             OAuth2AuthorizedClientRepository authorizedClientRepository) {

         // Customize
         //        OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient =
         DefaultRefreshTokenTokenResponseClient refreshTokenTokenResponseClient =
                new DefaultRefreshTokenTokenResponseClient();
         // 1 customize the pre-processing of the Token Request
         OAuth2RefreshTokenGrantRequestEntityConverter requestEntityConverter = new OAuth2RefreshTokenGrantRequestEntityConverter();
         // 1.1 customize only the parameters of the request
         //        requestEntityConverter.setParametersConverter(convert1);
         // 1.2 only add additional parameters
         //        requestEntityConverter.addParametersConverter(convert2);
         refreshTokenTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

         // 2 On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultRefreshTokenTokenResponseClient.setRestOperations() with a custom configured RestOperations. The default RestOperations is configured as follows:
         RestTemplate restTemplate = new RestTemplate(Arrays.asList(
                new FormHttpMessageConverter(),
                new OAuth2AccessTokenResponseHttpMessageConverter()));
         restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
         refreshTokenTokenResponseClient.setRestOperations(restTemplate);

         OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                     .authorizationCode()
                     .refreshToken(configurer -> configurer.accessTokenResponseClient(refreshTokenTokenResponseClient))
                     .build();
         // 此处使用了装饰者模式，DefaultOAuth2AuthorizedClientManager 在 authorize() 方法中装饰了 OAuth2AuthorizedClientProvider
         DefaultOAuth2AuthorizedClientManager authorizedClientManager =
         new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
         authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

         return authorizedClientManager;
     }

    /**
     * The DefaultOAuth2AuthorizedClientManager is designed to be used within the context of a HttpServletRequest. When operating outside of a HttpServletRequest context, use AuthorizedClientServiceOAuth2AuthorizedClientManager instead.
     * A service application is a common use case for when to use an AuthorizedClientServiceOAuth2AuthorizedClientManager. Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. An OAuth 2.0 Client configured with the client_credentials grant type can be considered a type of service application.
     * The following code shows an example of how to configure an AuthorizedClientServiceOAuth2AuthorizedClientManager that provides support for the client_credentials grant type:
     *
     * 权限授予类型：clientCredentials
     *
     * @param clientRegistrationRepository
     * @param authorizedClientService
     * @return
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager2(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientService authorizedClientService) {

        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .clientCredentials()
                        .build();
        // 此处使用了装饰者模式，DefaultOAuth2AuthorizedClientManager 在 authorize() 方法中装饰了 OAuth2AuthorizedClientProvider
        AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
     */

    /**
     * 权限授予类型：clientCredentials
     * @param clientRegistrationRepository
     * @param authorizedClientRepository
     * @return
     @Bean
     public OAuth2AuthorizedClientManager authorizedClientManager4(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {

         // Customize
         DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
                 new DefaultClientCredentialsTokenResponseClient();
         // 1 customize the pre-processing of the Token Request
         OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter = new OAuth2ClientCredentialsGrantRequestEntityConverter();
         // 1.1 customize only the parameters of the request
    //     requestEntityConverter.setParametersConverter(convert1);
         // 1.2 only add additional parameters
    //     requestEntityConverter.addParametersConverter(convert2);
         clientCredentialsTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

         // 2 On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultRefreshTokenTokenResponseClient.setRestOperations() with a custom configured RestOperations. The default RestOperations is configured as follows:
         RestTemplate restTemplate = new RestTemplate(Arrays.asList(
                 new FormHttpMessageConverter(),
                 new OAuth2AccessTokenResponseHttpMessageConverter()));
         restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
         clientCredentialsTokenResponseClient.setRestOperations(restTemplate);

         OAuth2AuthorizedClientProvider authorizedClientProvider =
                 OAuth2AuthorizedClientProviderBuilder.builder()
                 .authorizationCode()
                 .clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
                 .build();
         // 此处使用了装饰者模式，DefaultOAuth2AuthorizedClientManager 在 authorize() 方法中装饰了 OAuth2AuthorizedClientProvider
         DefaultOAuth2AuthorizedClientManager authorizedClientManager =
                 new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
         authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

         return authorizedClientManager;
     }
     */

    /**
     * 权限授予类型：password（带refreshToken）
     * @param clientRegistrationRepository
     * @param authorizedClientRepository
     * @return
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {
        // Customize
        DefaultPasswordTokenResponseClient passwordTokenResponseClient =
                new DefaultPasswordTokenResponseClient();
        // 1 customize the pre-processing of the Token Request
        OAuth2PasswordGrantRequestEntityConverter requestEntityConverter = new OAuth2PasswordGrantRequestEntityConverter();
        // 1.1 customize only the parameters of the request
//        requestEntityConverter.setParametersConverter(convert1);
        // 1.2 only add additional parameters
//        requestEntityConverter.addParametersConverter(convert2);
        passwordTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

        // 2 On the other end, if you need to customize the post-handling of the Token Response, you will need to provide DefaultPasswordTokenResponseClient.setRestOperations() with a custom configured RestOperations. The default RestOperations is configured as follows:
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
                new FormHttpMessageConverter(),
                new OAuth2AccessTokenResponseHttpMessageConverter()));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        passwordTokenResponseClient.setRestOperations(restTemplate);

        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .password(configurer -> configurer.accessTokenResponseClient(passwordTokenResponseClient))
                        .refreshToken()
                        .build();
        // 此处使用了装饰者模式，DefaultOAuth2AuthorizedClientManager 在 authorize() 方法中装饰了 OAuth2AuthorizedClientProvider
        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

//        The DefaultOAuth2AuthorizedClientManager is also associated with a contextAttributesMapper of type Function<OAuth2AuthorizeRequest, Map<String, Object>>,
//        which is responsible for mapping attribute(s) from the OAuth2AuthorizeRequest to a Map of attributes to be associated to the OAuth2AuthorizationContext.
//        This can be useful when you need to supply an OAuth2AuthorizedClientProvider with required (supported) attribute(s),
//        eg. the PasswordOAuth2AuthorizedClientProvider requires the resource owner’s username and password to be available in
//            OAuth2AuthorizationContext.getAttributes().
//        The following code shows an example of the contextAttributesMapper:

        // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
        //      map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
        authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

        return authorizedClientManager;
    }
     */

    /**
     * 权限授予类型：Jwt Bearer（准确的类型是 urn:ietf:params:oauth:grant-type:jwt-bearer）
     * @param clientRegistrationRepository
     * @param authorizedClientRepository
     * @return
     */
     @Bean
     public OAuth2AuthorizedClientManager authorizedClientManager(
             ClientRegistrationRepository clientRegistrationRepository,
             OAuth2AuthorizedClientRepository authorizedClientRepository) {
         // Customize
         DefaultJwtBearerTokenResponseClient jwtBearerTokenResponseClient =
                new DefaultJwtBearerTokenResponseClient();
         // 1 customize the pre-processing of the Token Request
         JwtBearerGrantRequestEntityConverter requestEntityConverter = new JwtBearerGrantRequestEntityConverter();
         // 1.1 customize only the parameters of the request
         //        requestEntityConverter.setParametersConverter(convert1);
         // 1.2 only add additional parameters
         //        requestEntityConverter.addParametersConverter(convert2);
         jwtBearerTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

         // 2 On the other end, if you need to customize the post-handling of the Token Response, you will need to provide JwtBearerGrantRequestEntityConverter.setRestOperations() with a custom configured RestOperations. The default RestOperations is configured as follows:
         RestTemplate restTemplate = new RestTemplate(Arrays.asList(
                 new FormHttpMessageConverter(),
                 new OAuth2AccessTokenResponseHttpMessageConverter()));
         restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
         jwtBearerTokenResponseClient.setRestOperations(restTemplate);

         JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider =
                 new JwtBearerOAuth2AuthorizedClientProvider();
         jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerTokenResponseClient);

         OAuth2AuthorizedClientProvider authorizedClientProvider =
                 OAuth2AuthorizedClientProviderBuilder.builder()
                         .provider(jwtBearerAuthorizedClientProvider)
                         .build();
         // 此处使用了装饰者模式，DefaultOAuth2AuthorizedClientManager 在 authorize() 方法中装饰了 OAuth2AuthorizedClientProvider
         DefaultOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
         authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

         //        The DefaultOAuth2AuthorizedClientManager is also associated with a contextAttributesMapper of type Function<OAuth2AuthorizeRequest, Map<String, Object>>,
         //        which is responsible for mapping attribute(s) from the OAuth2AuthorizeRequest to a Map of attributes to be associated to the OAuth2AuthorizationContext.
         //        This can be useful when you need to supply an OAuth2AuthorizedClientProvider with required (supported) attribute(s),
         //        eg. the PasswordOAuth2AuthorizedClientProvider requires the resource owner’s username and password to be available in
         //            OAuth2AuthorizationContext.getAttributes().
         //        The following code shows an example of the contextAttributesMapper:

         // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
         //      map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
         authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

         return authorizedClientManager;
     }

    private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
        return authorizeRequest -> {
            Map<String, Object> contextAttributes = Collections.emptyMap();
            HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
            String username = servletRequest.getParameter("username");
            String password = servletRequest.getParameter("password");
            if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
                contextAttributes = new HashMap<>();
                // `PasswordOAuth2AuthorizedClientProvider` requires both attributes
                contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
                contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
            }
            return contextAttributes;
        };
    }

    /**
     * Authorized Clients: WebClient integration for Servlet Environments
     *
     * 但是报错：org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'testController': Unsatisfied dependency expressed through field 'webClient'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'webClient' defined in class path resource [com/ian/config/OAuth2ClientConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.reactive.function.client.WebClient]: Factory method 'webClient' threw exception; nested exception is java.lang.IllegalStateException: No suitable default ClientHttpConnector found
     * 算了，暂时不管了，也用不到这个。
     *
     * @param authorizedClientManager
     * @return
    @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        return WebClient.builder()
                .apply(oauth2Client.oauth2Configuration())
                .build();
    }
     */

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        // 该构造器中可以放置多个 ClientRegistration
        return new InMemoryClientRegistrationRepository(
                githubClientRegistration(),
                giteeClientRegistration(),
                googleClientRegistration()
        );
    }

    private ClientRegistration githubClientRegistration() {
        return ClientRegistration.withRegistrationId("github")
                .clientId("810cb8e57c48403ba804")
                .clientSecret("b4c405151bf0dea5c8be46548fec730dcb339172")
//                .registrationId("registrationId_github")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // default value
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // default value
                // .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
                .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}") // default value
                // .scope("openid", "profile", "email", "address", "phone")
                .scope("read:user") // default value
                .authorizationUri("https://github.com/login/oauth/authorize") // default value
                .tokenUri("https://github.com/login/oauth/access_token") // default value
                .userInfoUri("https://api.github.com/user") // default value
                // .userNameAttributeName(IdTokenClaimNames.SUB)
                .userNameAttributeName("id") // default value
                // .jwkSetUri("")
                .clientName("Github") // default value
                .build();
    }

    private ClientRegistration giteeClientRegistration() {
        return ClientRegistration.withRegistrationId("gitee")
                .clientId("de91bdadc6b6b3599bebfa8755850fca0844aae44f13cc5e076110e961d4b59e")
                .clientSecret("6baa392358c93949b4f5e9e443dfa646746eb40c67ffa3eb87decb408d500b77")
//                .registrationId("registrationId_gitee")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // default value
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // default value
//                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//                .authorizationGrantType(AuthorizationGrantType.JWT_BEARER) // urn:ietf:params:oauth:grant-type:jwt-bearer
                // .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
                /*
                默认回调地址
                注意：1 该地址必须和gitee中配置的应用回调地址相同，否则gitee授权页面报错：存在错误，无效的登录回调地址；
                     其他的 自定义回调地址 的注意事项，这里统统不需要。
                 */
                .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}") // default value
                /*
                 自定义回调地址
                 注意：1 需要自定义一个Controller方法来处理该回调请求；
                      2 该地址必须和gitee中配置的应用回调地址相同，否则gitee授权页面报错：存在错误，无效的登录回调地址；
                      3 需要 在 SecurityConfig 配置类中配置放行该uri（如此处配置的 /authorization_code），
                            否则一到重定向url，由于没有权限，立即跳到login页面，然后又跳到gitee的授权页面，最终网页提示：你重定向次数过多。
                      4 在请求access_token时（发起post请求 https://gitee.com/oauth/token），必须携带该参数“redirect_uri”，
                            否则报错：org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: [no body]

                 最终发现，自定义回调地址，还是有问题，本地还是无法认证成功，只是能从gitee获取一些用户信息而已。
                    还是默认的回调地址吧。
                 */
//                .redirectUri("http://localhost:8060/authorization_code")
                // .scope("openid", "profile", "email", "address", "phone")
//                .scope("user") // default value，但是报错：存在错误，请求范围无效、未知或格式不正确
//                .authorizationUri("https://gitee.com/oauth/authorize") // default value

                /*
                 Customizing the Authorization Request
                 For the simple use case, where the additional request parameter is always the same for a specific provider, it may be added directly in the authorization-uri property.
                 可以在代码中配置（比如在这里配置），也可以在 application.yml 中配置
                 */
                .authorizationUri("https://gitee.com/oauth/authorize?prompt=consent")
                .tokenUri("https://gitee.com/oauth/token") // default value
                .userInfoUri("https://gitee.com/api/v5/user") // default value
                // .userNameAttributeName(IdTokenClaimNames.SUB)
                .userNameAttributeName("id") // default value
                // .jwkSetUri("")
                .clientName("Gitee") // default value
                .build();
    }

    private ClientRegistration googleClientRegistration() {
        return CommonOAuth2Provider.GOOGLE.getBuilder("google")
                .clientId("google-client-id")
                .clientSecret("google-client-secret")
//                .registrationId("registrationId_google")
                // ensure the ClientRegistration.redirectUri matches the custom Authorization Response baseUri
                .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
                /* The DefaultOAuth2AuthorizationRequestResolver also supports URI template variables for the redirect-uri using UriComponentsBuilder.
                     {baseUrl} resolves to {baseScheme}://{baseHost}{basePort}{basePath}
                     实际测试后，解析为这个url：http://localhost:8060/authorized/google
                     所以，{baseUrl} = {baseScheme}://{baseHost}{basePort}{basePath} = http://localhost:8060
                            其中，{basePath} = ""
                            {registrationId} = google
                */
                .redirectUri("{baseScheme}://{baseHost}{basePort}{basePath}/authorized/{registrationId}")

                /*
                OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. One of the strategies available is RP-Initiated Logout.
                If the OpenID Provider supports both Session Management and Discovery, the client may obtain the end_session_endpoint URL from the OpenID Provider’s Discovery Metadata. This can be achieved by configuring the ClientRegistration with the issuer-uri, as in the following example:
                 */
                .issuerUri("{baseUrl}")
                .build();
    }
}
